using System;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Stryker.Abstractions;
using Stryker.Abstractions.Reporting;
using Stryker.Core.MutationTest;
using Stryker.Core.ProjectComponents.TestProjects;
using Stryker.Abstractions.Options;
using Stryker.Abstractions.ProjectComponents;

namespace Stryker.Core.Initialisation;

public interface IProjectMutator
{
    IMutationTestProcess MutateProject(IStrykerOptions options, MutationTestInput input, IReporter reporters);
}

public class ProjectMutator : IProjectMutator
{
    private readonly ILogger _logger;
    private readonly IServiceProvider _serviceProvider;
    private readonly IMutationTestProcess _injectedMutationTestProcess;

    public ProjectMutator(ILogger<ProjectMutator> logger, IServiceProvider serviceProvider, IMutationTestProcess mutationTestProcess = null)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        _injectedMutationTestProcess = mutationTestProcess;
    }

    public IMutationTestProcess MutateProject(IStrykerOptions options, MutationTestInput input, IReporter reporters)
    {
        var process = _injectedMutationTestProcess ?? _serviceProvider.GetRequiredService<IMutationTestProcess>();
        process.Initialize(input, options, reporters);

        // Enrich test projects info with unit tests
        EnrichTestProjectsWithTestInfo(input.InitialTestRun, input.TestProjectsInfo);

        // mutate
        process.Mutate();

        return process;
    }

    private void EnrichTestProjectsWithTestInfo(InitialTestRun initialTestRun, ITestProjectsInfo testProjectsInfo)
    {
        var unitTests =
            initialTestRun.Result.TestDescriptions
            .Select(desc => desc.Case)
            // F# has a different syntax tree and would throw further down the line
            .Where(unitTest => Path.GetExtension(unitTest.CodeFilePath) == ".cs");

        foreach (var unitTest in unitTests)
        {
            var testFile = testProjectsInfo.TestFiles.SingleOrDefault(testFile => testFile.FilePath == unitTest.CodeFilePath);
            if (testFile is not null)
            {
                var lineSpan = testFile.SyntaxTree.GetText().Lines[unitTest.LineNumber - 1].Span;
                var nodesInSpan = testFile.SyntaxTree.GetRoot().DescendantNodes(lineSpan);
                var node = nodesInSpan.First(n => n is MethodDeclarationSyntax);
                testFile.AddTest(unitTest.Id, unitTest.FullyQualifiedName, node);
            }
            else
            {
                _logger.LogDebug("Could not locate unit test in any testfile. This should not happen and results in incorrect test reporting.");
            }
        }
    }
}
